// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: hyuan

#include "coin2/exchange/deribit_base/api_order/native_private_client.h"

#include <memory>
#include <string>

#include "coin2/exchange/base/api_util/url_params_builder.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/exchange/deribit_base/api_util/rate_tracker.h"
#include "coin2/exchange/deribit_futures/symbology/product.h"
#include "coin2/exchange/deribit_options/symbology/product.h"

#define CHECK_API_LIMIT(action_type)                                                      \
  if (!HasEnoughRateLimit(ts, action_type)) {                                             \
    LOG_EVERY_N(WARNING, 50) << "Hit rate limit or api temporary disabled, " << __func__; \
    api_block_info_[__func__]++;                                                          \
    return {};                                                                            \
  }

namespace {
using ActionType = deribit::api_util::DeribitRateTracker::ActionType;
using coin2::exchange::deribit::api_util::DeribitRateTracker;
using AsioHttpContextList = std::list<std::shared_ptr<AsioHttpContext>>;
using RestParams = coin2::exchange::order::DeribitFuturesRestParams;
}  // namespace

namespace coin2::exchange::deribit_futures::api_order {

auto DeribitFuturesNativePrivateClient::GetRestOrderParams(
    const OrderSpec& order_spec,
    int64_t proc_order_id) -> RestParams {
  auto params = RestParams();

  if (mea_.market == coin::proto::Futures) {
    using coin2::exchange::deribit_futures::symbology::DeribitFuturesProduct;
    auto product =
        DeribitFuturesProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    params.set_amount(formatter.FormatQty(order_spec.qty));
  } else if (mea_.market == coin::proto::Options) {
    using coin2::exchange::deribit_options::symbology::DeribitOptionsProduct;
    auto product =
        DeribitOptionsProduct::FromStr(order_spec.product, order_spec.order_created_time);
    auto product_holder = GetProductHolder(product, order_spec.order_created_time);
    auto formatter = product_holder->product_info().order_formatter();
    params.set_price(formatter.FormatPrice(order_spec.price));
    params.set_amount(formatter.FormatQty(order_spec.qty));
  } else {
    CHECK(false) << "Unknown market type.";
  }

  params.set_instrument_name(order_spec.product);
  params.set_label(std::to_string(proc_order_id));
  if (order_spec.order_type == OrderType::MARKET_ORDER) {
    params.set_type("market");
  } else {
    params.set_type("limit");
  }

  if (order_spec.order_side == OrderSide::BUY_ORDER) {
    params.set_side("buy");
  } else if (order_spec.order_side == OrderSide::SELL_ORDER) {
    params.set_side("sell");
  } else {
    NOTREACHED() << "Unknown order side: " << static_cast<int>(order_spec.order_side);
  }

  if (order_spec.duration == coin::proto::OrderDuration::FOK_ORDER) {
    params.set_time_in_force("fill_or_kill");
  } else if (order_spec.duration == coin::proto::OrderDuration::IOC_ORDER) {
    params.set_time_in_force("immediate_or_cancel");
  } else {
    params.set_time_in_force("good_til_cancelled");
  }

  if (order_spec.post_only) {
    params.set_post_only("true");
    params.set_time_in_force("good_til_cancelled");
  }

  if (order_spec.reduce_only) {
    params.set_reduce_only("true");
  } else {
    params.set_reduce_only("false");
  }
  return params;
}

auto DeribitFuturesNativePrivateClient::GetRestCancelParams(const IOrderContext& order_context)
    -> RestParams {
  auto params = RestParams();
  const std::string& product_str = order_context.order_spec().product;
  if (order_context.has_exchange_order_id()) {
    params.set_order_id(order_context.exchange_order_id().substr(product_str.length()));
  } else {
    params.set_order_id(std::to_string(order_context.proc_order_id()));
  }
  return params;
}

DeribitFuturesNativePrivateClient::DeribitFuturesNativePrivateClient(
    const MarketExchangeApi& mea,
    const OrderConnectionConfig& config_override) {
  mea_ = mea;
  // default config
  order::OrderConnectionConfig config;
  config.set_rest_host("www.deribit.com");
  config.set_rest_host_port(443);
  // user supplied config override default config
  config.MergeFrom(config_override);
  LOG(INFO) << "[DeribitFuturesNativePrivateClient] config: " << config.DebugString();

  auths_.Construct<DeribitRestAuth, order::OrderConnectionConfig>(config);

  auth_path_ = "/api/v2/public/auth";
  account_path_ = "/api/v2/private/get_subaccounts";
  position_path_ = "/api/v2/private/get_positions";
  trade_path_ = "/api/v2/private/get_user_trades_by_currency";
  open_orders_path_ = "/api/v2/private/get_open_orders_by_currency";
  hist_orders_path_ = "/api/v2/private/get_order_history_by_currency";

  place_order_path_buy_ = "/api/v2/private/buy";
  place_order_path_sell_ = "/api/v2/private/sell";
  cancel_order_path_ = "/api/v2/private/cancel";

  deposits_path_ = "/api/v2/private/get_deposits";
  transfers_path_ = "/api/v2/private/get_transfers";
  withdrawals_path_ = "/api/v2/private/get_withdrawals";

  LOG(INFO) << "DeribitFutures: " << mea_.String();
  if (mea_.market == coin::proto::Futures) {
    kind_ = "future";
  } else if (mea_.market == coin::proto::Options) {
    kind_ = "option";
  } else {
    CHECK(false) << "Unknown market type!" << static_cast<int>(mea_.market);
  }

  rate_tracker_ = std::make_unique<DeribitRateTracker>(mea_, config);
  start_time_ = GetCurrentTimestamp();

  local_auth_ = dynamic_cast<DeribitRestAuth*>(GetAuth());
  SYSTEM_CRITICAL_ERROR(local_auth_, "failed to init deribit auth");
}

void DeribitFuturesNativePrivateClient::Prepare() {
  mb_->Subscribe("deribit/token", [this](const std::string&, void* data) {
    const std::string* data_str = reinterpret_cast<const std::string*>(data);
    auto vec = Split(*data_str, "\n");
    local_auth_->set_token(vec[0], vec[1]);
  });
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::GenerateOrRefreshAuth() {
  auto now = GetCurrentTimestamp();
  if (local_auth_->has_auth_token() &&
      now < local_auth_->last_set_token_ts() + 300'000'000'000LL /* 5mins */) {
    // not time to refresh yet, refresh every 5 mins
    return {};
  }

  if (local_auth_->get_is_refreshing_token() &&
      now < local_auth_->last_refresh_ts() + 5'000'000'000LL /* 5s */) {
    // it's refreshing, wait 5s
    return {};
  }

  local_auth_->set_is_refreshing_token();
  UrlParamsBuilder builder;
  builder.PutGet("grant_type", "client_credentials");
  builder.PutGet("client_id", local_auth_->GetAccessKey());
  builder.PutGet("client_secret", local_auth_->GetSecretKey());
  auto* context = local_auth_->CreateRequestByGetWithSignature(auth_path_, &builder);
  context->query_type = OrderSubTopicId::LISTEN_KEY;
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryAccountSummary(
    const std::string& currency) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("currency", currency);
  const char* path = "/api/v2/private/get_account_summary";
  auto* context = GetAuth()->CreateRequestByGetWithSignature(path, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryAccountBalance() {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("with_portfolio", "true");
  auto* context = GetAuth()->CreateRequestByGetWithSignature(account_path_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_BALANCE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryAccountPosition(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("kind", kind_);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(position_path_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ACCOUNT_POSITION;
  context->extra_info = params.currency();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryOpenOrders(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("kind", kind_);
  builder.PutGet("type", "all");
  auto* context = GetAuth()->CreateRequestByGetWithSignature(open_orders_path_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_OPEN_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryHistoryOrders(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("kind", kind_);
  builder.PutGet("count", 50);
  builder.PutGet("include_unfilled", "true");
  auto* context = GetAuth()->CreateRequestByGetWithSignature(hist_orders_path_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_ALL_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryTrades(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  DLOG_EVERY_N(INFO, 50) << DebugString();
  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::QUERY);
  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("kind", kind_);
  builder.PutGet("start_id", params.start_id());
  builder.PutGet("end_id", params.end_id());
  builder.PutGet("count", 50);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(trade_path_, &builder);
  Push(1, ts, ActionType::QUERY);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::REST_FILL_ORDERS;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::PlaceOrder(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::PLACE);
  CHECK((params.side() == "buy") || (params.side() == "sell"));
  UrlParamsBuilder builder;
  builder.PutGet("instrument_name", params.instrument_name());
  builder.PutGet("amount", params.amount());
  builder.PutGet("type", params.type());
  builder.PutGet("label", params.label());
  builder.PutGet("price", params.price());
  builder.PutGet("time_in_force", params.time_in_force());
  // builder.PutGet("max_show", params.amount);
  builder.PutGet("post_only", params.post_only());
  builder.PutGet("reject_post_only", "true");  // fixed value
  builder.PutGet("reduce_only", params.reduce_only());
  const std::string& place_order_path =
      (params.side() == "buy") ? place_order_path_buy_ : place_order_path_sell_;
  AsioHttpContext* context;
  context = GetAuth()->CreateRequestByGetWithSignature(place_order_path, &builder);
  Push(1, ts, ActionType::PLACE);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::SUBMIT_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::CancelOrder(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  auto ts = GetCurrentTimestamp();
  CHECK_API_LIMIT(ActionType::CANCEL);
  UrlParamsBuilder builder;
  builder.PutGet("order_id", params.order_id());
  auto* context = GetAuth()->CreateRequestByGetWithSignature(cancel_order_path_, &builder);
  Push(1, ts, ActionType::CANCEL);
  api_req_info_[__func__]++;

  context->query_type = OrderSubTopicId::CANCEL_RESPONSE;
  context->extra_info = DebugString();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::PlaceOrderNorm(
    const OrderExecution& order_exe) {
  const auto& order_spec = *order_exe.order_spec;
  const auto proc_order_id = order_exe.proc_order_id;
  auto params = GetRestOrderParams(order_spec, proc_order_id);
  return PlaceOrder(params);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::CancelOrderNorm(
    const OrderExecution& order_exe) {
  auto params = GetRestCancelParams(*order_exe.oc);
  return CancelOrder(params);
}

std::string DeribitFuturesNativePrivateClient::DebugString() const {
  double delta_t = (GetCurrentTimestamp() - start_time_) / 1.0e9;

  std::stringstream ss;
  ss << "DeribitFuturesNativePrivateClient::DebugString\n";
  ss << "api info\n";
  for (auto& [name, value] : api_req_info_) {
    int32_t blocked = 0;
    if (api_block_info_.count(name) > 0) {
      blocked = api_block_info_.at(name);
    }
    int32_t total_req = value + blocked;
    ss << name << " (total:" << total_req << ", succeeded: " << value << ", blocked: " << blocked
       << ", req_rate: " << value / delta_t << ", block_rate: " << blocked / delta_t << ")\n";
  }
  return ss.str();
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryDeposits(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("count", 1000);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(deposits_path_, &builder);

  context->query_type = DeribitOrderSubTopicId::DEPOSIT;
  context->extra_info = params.currency();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryWithdrawals(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("count", 1000);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(withdrawals_path_, &builder);

  context->query_type = DeribitOrderSubTopicId::WITHDRAWAL;
  context->extra_info = params.currency();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::QueryTransfers(
    const RestParams& params) {
  if (!has_auth_token()) {
    LOG(INFO) << "Deribit auth token not ready!";
    return {};
  }

  UrlParamsBuilder builder;
  builder.PutGet("currency", params.currency());
  builder.PutGet("count", 1000);
  auto* context = GetAuth()->CreateRequestByGetWithSignature(transfers_path_, &builder);

  context->query_type = OrderSubTopicId::TRANSFER;
  context->extra_info = params.currency();
  return std::shared_ptr<AsioHttpContext>(context);
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::SubmitOrderExecution(
    const OrderExecutionProto& order) {
  return nullptr;
}

template <typename Func>
AsioHttpContextList FillRequest(Func func) {
  AsioHttpContextList list;
  RestParams params;
  static const std::vector<std::string> currency_list = {"BTC", "ETH", "USDC"};
  for (auto& currency : currency_list) {
    params.set_currency(currency);
    list.emplace_back(func(params));
  }

  return list;
}

AsioHttpContextList DeribitFuturesNativePrivateClient::SubmitRestQueryList(
    const RestQueryProto& query) {
  Prepare();
  AsioHttpContextList list;
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_TRANSFERS: {
      list = FillRequest(std::bind(
          &DeribitFuturesNativePrivateClient::QueryTransfers,
          this,
          std::placeholders::_1));
      break;
    }
    case RestQueryProto::QUERY_WITHDRAW: {
      list = FillRequest(std::bind(
          &DeribitFuturesNativePrivateClient::QueryWithdrawals,
          this,
          std::placeholders::_1));
      break;
    }
    case RestQueryProto::QUERY_DEPOSIT: {
      list = FillRequest(std::bind(
          &DeribitFuturesNativePrivateClient::QueryDeposits,
          this,
          std::placeholders::_1));
      break;
    }
    case RestQueryProto::INIT_QUERY_INFO: {
      list.emplace_back(GenerateOrRefreshAuth());
      break;
    }
    default: {
      return list;
    }
  }

  return list;
}

std::shared_ptr<AsioHttpContext> DeribitFuturesNativePrivateClient::SubmitRestQuery(
    const RestQueryProto& query) {
  RestParams params;
  switch (query.type()) {
    case RestQueryProto::QUERY_ACCOUNT_BALANCE: {
      return GenerateOrRefreshAuth();
    }
    case RestQueryProto::QUERY_ACCOUNT_POSITION: {
      return QueryAccountPosition(params);
    }
    case RestQueryProto::QUERY_OPEN_ORDERS: {
      params.set_type("future");
      params.set_type("limit");
      return QueryOpenOrders(params);
    }
    default: {
      return nullptr;
    }
  }
}

}  // namespace coin2::exchange::deribit_futures::api_order
